/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import { DirectorySnapshot } from '../core/directory-snapshot.js';
import { FileSnapshot } from '../core/file-snapshot.js';
import { BasicFileSnapshot } from '../core/filesystem-snapshot.js';
import { FileHasher, FileMetaHasher } from '../util/file-hasher.js';
import { FileMetaData } from '../util/file-metadata.js';
import { FsOptions } from '../util/file-set.js';
import { FileInfo, FileOptions, FileVisitor, walkFileTree } from '../util/file-tree.js';
import { FileType } from '../util/file-type.js';
import { Hash } from '../util/hash.js';

import { FileSystemSnapshotGenerator } from './filesystem-snapshot-generator.js';

class FileVisitorImpl implements FileVisitor {
  rootSnapShot: BasicFileSnapshot | undefined = undefined;
  stack: DirectorySnapshot[] = [];
  fileHasher: FileHasher;

  constructor(fileHasher: FileHasher) {
    this.fileHasher = fileHasher;
  }

  preVisitDirectory(fileInfo: FileInfo): void {
    const directorySnapshot = new DirectorySnapshot(fileInfo.name, fileInfo.path, fileInfo.type, fileInfo.isSymbolicLink);

    if (this.rootSnapShot === undefined) {
      this.rootSnapShot = directorySnapshot;
    }

    if (this.stack.length > 0) {
      const parentDir = this.stack[this.stack.length - 1];
      parentDir.children.push(directorySnapshot);
    }

    this.stack.push(directorySnapshot);
  }

  visitFile(fileInfo: FileInfo): void {
    const fileMetaData = new FileMetaData(fileInfo.size, fileInfo.lastModifiedTime);
    const fileSnapshot = new FileSnapshot(fileInfo.name, fileInfo.path, fileInfo.type, fileInfo.isSymbolicLink, fileMetaData);
    fileSnapshot.hashValue = this.fileHasher.hash(fileInfo.path, fileMetaData);

    if (this.rootSnapShot === undefined) {
      this.rootSnapShot = fileSnapshot;
    }

    if (this.stack.length > 0) {
      const parentDir = this.stack[this.stack.length - 1];
      parentDir.children.push(fileSnapshot);
    }
  }

  postVisitDirectory(fileInfo: FileInfo): void {
    const directorySnapshot = this.stack.pop();
    if (directorySnapshot && directorySnapshot.children.length > 0) {
      // 为了保证稳定性，子节点按名字排序
      directorySnapshot.children.sort((previous: BasicFileSnapshot, current: BasicFileSnapshot) => {
        return previous.name.localeCompare(current.name);
      });

      // 文件夾的 hashValue 取所有子节点的 name + hashValue 拼接然后再hash
      let hashSum = '';
      directorySnapshot.children.forEach((child) => {
        hashSum += child.name + child.hashValue;
      });
      directorySnapshot.hashValue = Hash.hash(hashSum);
    }
  }

  visitFileFailed(path: string, error: any): void {
    // no op
  }
}

export class DefaultFilesystemSnapshotGenerator implements FileSystemSnapshotGenerator {
  private readonly fileHasher: FileHasher;

  constructor(fileHasher = new FileMetaHasher()) {
    this.fileHasher = fileHasher;
  }

  private parseFileSystemSnapshot(jsonObj: any): BasicFileSnapshot | undefined {
    const type = jsonObj['type'];
    const hashValue = jsonObj['hashValue'] ?? '';
    const name = jsonObj['name'] ?? '';
    const path = jsonObj['path'] ?? '';
    const isSymbolicLink = jsonObj['isSymbolicLink'] ?? '';

    if (type === FileType.FILE.toString()) {
      const fileMetaData = new FileMetaData();
      const fileMetaDataObj = jsonObj['fileMetaData'];
      if (fileMetaDataObj) {
        const size = fileMetaDataObj['size'] ?? 0;
        const lastModifiedTime = fileMetaDataObj['lastModifiedTime'] ?? 0;
        fileMetaData.size = size;
        fileMetaData.lastModifiedTime = lastModifiedTime;
      }

      const fileSnapshot = new FileSnapshot(name, path, FileType.FILE, isSymbolicLink, fileMetaData);
      fileSnapshot.hashValue = hashValue;
      return fileSnapshot;
    } else if (type === FileType.DIRECTORY.toString()) {
      const directorySnapshot = new DirectorySnapshot(name, path, FileType.DIRECTORY, isSymbolicLink);
      directorySnapshot.hashValue = hashValue;

      const children = jsonObj['children'] ?? [];
      for (const child of children) {
        const childSnapshot = this.parseFileSystemSnapshot(child);
        if (childSnapshot !== undefined) {
          directorySnapshot.children.push(childSnapshot);
        }
      }
      return directorySnapshot;
    } else {
      return undefined;
    }
  }

  /**
   * Snapshot a file or directory
   *
   * @param filepath filepath
   * @param options FsOptions for filter file
   */
  snapshot(filepath: string, options?: FsOptions): BasicFileSnapshot {
    const visitor = new FileVisitorImpl(this.fileHasher);
    const fileOptions: FileOptions = {};
    if (options && options.test) {
      fileOptions.filter = options.test;
    }
    walkFileTree(filepath, visitor, fileOptions);
    return visitor.rootSnapShot ?? new DirectorySnapshot('emptyroot', '', FileType.UNKNOWN, false);
  }

  loadSnapshotFromJson(jsonStr: string): BasicFileSnapshot | undefined {
    let jsonObj: any;
    try {
      jsonObj = JSON.parse(jsonStr);
    } catch (e) {
      return undefined;
    }
    return this.parseFileSystemSnapshot(jsonObj);
  }

  loadSnapshotCacheFromJson(jsonStr: string): Map<string, BasicFileSnapshot> | undefined {
    let jsonObj: any;
    try {
      jsonObj = JSON.parse(jsonStr);
    } catch (e) {
      return undefined;
    }

    const map = new Map<string, BasicFileSnapshot>();
    Object.keys(jsonObj).forEach((key: string) => {
      const fileSystemSnapshot = this.parseFileSystemSnapshot(jsonObj[key]);
      if (fileSystemSnapshot !== undefined) {
        map.set(key, fileSystemSnapshot);
      }
    });
    return map;
  }

  serializeSnapshotToJson(map: Map<string, BasicFileSnapshot>): string {
    let json = '{';
    map.forEach((value, key) => {
      json += JSON.stringify(key) + ':' + JSON.stringify(value) + ',';
    });
    if (json.length > 1) {
      json = json.slice(0, json.length - 1);
    }
    json += '}';
    return json;
  }
}
